#include "xml_parser.h"
#include "exceptions.h"
#include "converter.h"
#include "str.h"

map<const char*, const char*> xml_parser::special_tags;
const string xml_parser::allowed_symbols = "_-.:";

void xml_parser::reset_the_buffer() {
  delete[] buffer;
  buffer = 0;
} // xml_parser::reset_the_buffer

void xml_parser::open(const string &file_name) {
  if (xml_file != 0)
    close();

  xml_file = fopen(file_name.c_str(), "rb"); // read-only + binary
  if (xml_file == 0) {
    string msg = "XML file '%1' not found!";
    str::format(msg, arg<string>(file_name));
    throw xml_exception(msg);
  } // if
} // xml_parser::open

void xml_parser::close() {
  if (fclose(xml_file) == EOF)
    throw xml_exception("XML file could not be closed successfully");

  xml_file = 0;
} // xml_parser::close

bool xml_parser::get_next_tag(string &output, size_t &index) {
  output.resize(0);
  const char *i = strchr(buffer + index, '<');
  if (i == 0)
    return false; // no opening tag found

  const char *j = 0;

  auto end_it = special_tags.cend();
  for (auto it = special_tags.cbegin(); it != end_it; ++it) {
    if (strncmp(i, it->first, strlen(it->first)) == 0) {

      j = strstr(i + strlen(it->first), it->second); // finds the closing tag
      if (j == 0) {
        error_occurred = true;
        return false; // no closing tag found!
      } // if

      index = j - buffer + strlen(it->second);
      return get_next_tag(output, index); // ignores XML header tag
    } // if
  } // for it

  j = strchr(i, '>');
  if (j == 0) {
    error_occurred = true;
    return false; // no closing tag found
  } // if

  const size_t len = j - i + 1;
  output.assign(i, len);

  index = j - buffer + 1; // moves [advances] the "index" to the position right after the closing tag

  if (output.length() > 2 && output[1] == '/')
    return false; // closing tag
  else
    return true; // opening tag
} // xml_parser::get_next_tag

size_t xml_parser::get_tag_name(const string &input, string &output) const {
  size_t i = 1; // start index

  while (i < input.length() &&
          ((unsigned char) input[i] > 127 ||
           isalnum((unsigned char) input[i]) != 0 ||
           allowed_symbols.find(input[i]) != string::npos))
    ++i;

  output.assign(input, 1, i - 1);
  return i;
} // xml_parser::get_tag_name

void xml_parser::ignore_spaces(const string &input, size_t &index) const {
  static const string allowed_space_symbols = " \t\n\r";
  const size_t len = input.length();
  while (index < len && allowed_space_symbols.find(input[index]) != string::npos)
    ++index;

} // xml_parser::ignore_spaces

void xml_parser::get_attribute_name(const string &input, size_t &index, string &output) const {
  ignore_spaces(input, index);
  if (index >= input.length()) {
    output.clear();
    return;
  } // if

  const size_t start = index;
  while (index < input.length() &&
         ((unsigned char) input[index] > 127 ||
          isalnum((unsigned char) input[index]) != 0 ||
          allowed_symbols.find(input[index]) != string::npos))
    ++index;

  if (index < input.length())
    output.assign(input, start, index - start);
  else
    output.clear();
} // xml_parser::get_attribute_name

void xml_parser::add_attributes_to_node(const string &input, xml_node *node, size_t &index) const {
  string name;
  const size_t len = input.length();

  do {
    get_attribute_name(input, index, name); // attribute's name
    if (name.empty() == true)
      return; // no attribute

    ignore_spaces(input, index); // spaces before =
    if (index < len && input[index] == '=')
      ++index;
    else
      continue;

    ignore_spaces(input, index); // spaces after =
    if (index >= len)
      return;

    char start_symbol = 0;
    if (input[index] == '"' || input[index] == '\'') {
      start_symbol = input[index];
      ++index;
    } else
      continue;

    const size_t start = index;

    while (index < len) {
      if (start_symbol != 0) {
        if (input[index] != start_symbol || (index > 0 && input[index - 1] == '\\')) // escape comma
          ++index;
        else
          break;
      } else {
        if (input[index] != ' ' && input[index] != '/' && input[index] != '>')
          ++index;
        else
          break;
      } // else
    } // while

    xml_attribute *xa = new xml_attribute(name, global_empty_string); // attribute's name
    string &xa_content = const_cast<string&>(xa->content);
    xa_content.assign(input, start, index - start); // attribute's content
    node->add_attribute(xa);

    ++index;
  } while (true);
} // xml_parser::add_attributes_to_node

void xml_parser::add_content_to_node(xml_node *node, size_t &index) const {
  const size_t start = index;

  while (buffer[index] != '\0' && buffer[index] != '<')
    ++index;

  if (strncmp(buffer + index, "<![CDATA[", strlen("<![CDATA[")) == 0) {
    while (buffer[index] != '\0' && strncmp(buffer + index, "]]>", strlen("]]>")) != 0)
      ++index;

    index += strlen("]]>");
  } // if

  if (buffer[index] != '\0') {
    node->content.assign((const char*) &buffer[start], index - start);
    converter::trim(node->content, " \t\n\r", converter::trim_left_and_right);
  } // if
} // xml_parser::add_content_to_node

bool xml_parser::is_empty_element(const string &input) const {
  const size_t len = input.length();
  return (len > 2 && input[len - 2] == '/');
} // xml_parser::is_empty_element

void xml_parser::parse_children(xml_node *node, size_t &index, bool is_root_node) {
  string tmp;

  do { // this loop parses all children of the current node
    const bool is_opening_tag = get_next_tag(tmp, index);
    if (is_opening_tag == false || error_occurred == true || tmp.empty() == true)
      return;

    xml_node *n = 0;

    if (is_root_node == true)
      n = node;
    else {
      n = new xml_node();
      node->add_child(n);
    } // else

    size_t i = get_tag_name(tmp, n->name);
    add_attributes_to_node(tmp, n, i);

    if (is_empty_element(tmp) == false) {
      add_content_to_node(n, index);
      parse_children(n, index, false); // parses all sub-children of the current node (recursive call)
    } // if

  } while (true);
} // xml_parser::parse_children

void xml_parser::parse_and_check(xml_node *node) {
  size_t s_index = 0;
  this->error_occurred = false;

  parse_children(node, s_index, true);

  if (error_occurred == true)
    throw xml_exception("An error occurred while parsing the XML file! Make sure that the XML file is a valid XML document.");
} // xml_parser::parse_and_check

void xml_parser::parse_file(const string &file_name, xml_node *node) {
  if (node == 0)
    return;

  node->destroy();

  open(file_name); // file encoding can be ignored here

  if (fseek(xml_file, 0, SEEK_END) == 0) { // go to the end of the file
    const long len = ftell(xml_file);

    if (fseek(xml_file, 0, SEEK_SET) == 0) { // go to the begining of the file
      reset_the_buffer();
      buffer = new char[len + 1];
      const size_t count = fread(buffer, sizeof(char), len, xml_file);
      close();

      buffer[len] = '\0';

      if (count == 0) {
        string msg = "Some error(s) happened while reading the XML file '%1'!";
        str::format(msg, arg<string>(file_name));
        throw xml_exception(msg);
      } // if
    } // if
  } // if

  parse_and_check(node); // if no exception happens here, then
  reset_the_buffer(); // we release the buffer, if not then the destructor will do that later.
} // xml_parser::parse_file

void xml_parser::parse_string(const string &xml_string, xml_node *node) {
  if (node == 0)
    return;

  node->destroy();

  reset_the_buffer();
  buffer = new char[xml_string.length() + 1];
  strcpy(buffer, xml_string.c_str());

  parse_and_check(node); // if no exception happens here, then
  reset_the_buffer(); // we release the buffer, if not then the destructor will do that later.
} // xml_parser::parse_string

xml_parser::xml_parser() : xml_file(0), buffer(0), error_occurred(false) {
  if (special_tags.empty() == true) {
    special_tags["<?"] = "?>"; // ignores XML header tag
    special_tags["<!--"] = "-->"; // ignores XML comments
    special_tags["<!DOCTYPE"] = ">"; // ignores XML doctype
    special_tags["<![CDATA["] = "]]>"; // (Unparsed) Character Data
  } // if
} // xml_parser::xml_parser

xml_parser::~xml_parser() {
  reset_the_buffer(); // we must call the "reset_the_buffer()" method here, to make sure that the buffer will be released
} // xml_parser::~xml_parser
